# guile-openai

This is a [GNU Guile][guile] client for the [OpenAI API][openai-api].
It currently implements the Chat and Image APIs, with support for chat
response streaming and inline image display in a [Geiser][geiser]
REPL.

## Installation

### Guix

An up-to-date [GNU Guix][guix] package definition is provided in the
repository, so you can install it into your profile with the
following:

```shell
guix package -f path/to/guile-openai/guix.scm
```

Alternatively, you can jump straight into an interactive environment
by loading it with [Guile Studio][guile-studio]:

```shell
guix shell guile guile-studio -f path/to/guile-openai/guix.scm -- guile-studio
```

### Autotools

A conventional build system is also provided:

``` shell
cd path/to/guile-openai
./bootstrap
./configure && make
```

## Documentation

### Quick start

The library is designed for convenient interactive use, so many useful
settings are defined as parameters.  Reasonable defaults are provided,
so all you need to get started is an API key and a Guile REPL.

To get an API key, you'll need to sign up for an account with OpenAI.
Currently new accounts get a certain number of free requests, but
ultimately the OpenAI API is a paid service.

```scheme
(use-modules (openai))

;; Set your OpenAI API key
(openai-api-key "MYVERYSECRETAPIKEY")

;; Simple chat example
(openai-chat "Why is software freedom important?")

;; Simple image example
(openai-image "A cute drawing of a kid in a gnu onesie programming a
 robot with their laptop computer")

;; Generate a prompt to generate an image
(let ((rsp (openai-chat "A short paragraph describing a cute mascot for
 the GNU Guile Scheme programming language")))
  (display rsp)
  (newline)
  (openai-image (chat-content rsp)))

;; Generate an image and a variation of that image
(let* ((original (openai-image "A whimsical scheme wizard."))
       (variation (openai-image-variation original)))
  (values original variation))

;; Interactive chat example
(use-modules (ice-9 readline))

(define default-prompt
  "You are an AI assistant embedded in a GNU Guile Scheme REPL, which
 answers user enquiries in a helpful and playful manner while strongly
 advocating for Free Software and the principles of user freedom.")

(define* (interactive-chat #:optional (prompt default-prompt))
  (let loop ((msgs `((system . ,prompt))))
    (let ((query (readline "> ")))
      (if (or (eof-object? query)
              (string-null? query))
          msgs
          (let* ((msgs (append msgs `((user . ,query))))
                 (chat (openai-chat msgs)))
            (newline)
            (display chat)
            (display "\n\n")
            (let* ((text (chat-content chat))
                   (msgs (append msgs `((assistant . ,text)))))
              (loop msgs)))))))
```

### Configuration

- (**openai-base-uri** "https://api.openai.com/")

  Parameter for the URI of the OpenAI API endpoint.

- (**openai-api-key** *unspecified*)

  Parameter for the API key to use for requests.

- (**openai-organization** *unspecified*)

  Parameter for an optional organization ID to include in requests.

- (**openai-default-headers** '())

  Parameter for additional HTTP headers to include in requests.

- (**openai-default-user** *unspecified*)

  Parameter for a default value for the `#:user` argument of requests.

### Chat API

- (**openai-chat** prompt *[keyword arguments]*)

  Send a chat completion request.  Returns a *chat* record.

  The *prompt* can be a string, which will be sent as a user message.
  Alternatively, prompt can be a list of `(role . content)` pairs,
  where content is a string and role is a symbol `system`, `user`, or
  `assistant`.

  The keyword arguments correspond to the request parameters described
  in the [chat completion request][chat-request] documentation:

  - `#:n`

    The number of responses to generate, returned as multiple values.

  - `#:stream?`

    Whether to stream the response(s), defaults to `#t`.

  - `#:model`

    A symbol or string identifying the model to use.  Defaults to
    `gpt-3.5-turbo`, but if you're lucky you might be able to use
    `gpt-4` or `gpt-4-32k` here.

  - `#:temperature`

    The sampling temperature to use, a number between 0 and 2.

  - `#:top-p`

    An alternative sampling parameter, a number between 0 and 1.

  - `#:user`

    An optional username to associate with this request.

  The `#:stop`, `#:max-tokens`, `#:logit-bias`, `#:presence-penalty`,
  `#:frequency-penalty` parameters are implemented but untested.

- (**openai-default-chat-model** 'gpt-3.5-turbo)

  Parameter for the default `#:model` value.

- (**openai-default-chat-temperature** *unspecified*)

  Parameter for the default `#:temperature` value.

- (**openai-default-chat-top-p** *unspecified*)

  Parameter for the default `#:top-p` value.

- (**openai-default-chat-stream?** #t)

  Parameter for the default `#:stream?` value.

- (**chat?** chat)

  Type predicate for chat responses.

- (**chat-content** chat)

  Return the chat response as a string.

- (**chat-stream** chat)

  Return the chat response as a stream of strings.

### Image API

- (**openai-image** prompt *[keyword arguments]*)

  Send an image generation request.  Returns an *image* record.

  The *prompt* must be a string.

  The keyword arguments correspond to the request parameters described
  in the [image generation request][image-request] documentation:

  - `#:n`

    The number of images to generate, returned as multiple values.

  - `#:size`

    A number specifying the generated image size, either 256, 512,
    or 1024.  The symbols `256x256`, `512x512` and `1024x1024` are
    also supported.

  - `#:format`

    How the image data will be returned, either `url` (hosted on a
    cloud server), or `b64` (embedded in the response message, the
    default).  In either case, the image data will be fetched and
    stored locally for display.

  - `#:user`

    An optional username to associate with this request.

- (**openai-image-edit** image prompt *[keyword arguments]*)

  Send an image edit request.  Returns an *image* record.

  The *image* can be an *image* record or a string path to the image
  file to be varied.  The API requires the image to be a valid PNG
  file, less than 4MB, and square.

  The *prompt** must be a string describing the changes to be made to
  the image.

  The keyword arguments correspond to the request parameters described
  in the [image edit request][image-edit-request] documentation:

  - `#:mask`

  An *image* record or a string path to an image file to use as the
  edit mask.  The model will only edit pixels which are fully
  transparent (ie. alpha 0) in the mask image.  The mask must have the
  same dimensions as IMAGE.  The default is fully opaque, so no pixels
  will be edited and a copy of the original image will be returned.

  - `#:n`, `#:size`, `#:format`, `#:user`

    See `openai-image`.

- (**openai-image-variation** image *[keyword arguments]*)

  Send an image variation request.  Returns an *image* record.

  The *image* can be an *image* record or a string path to the image
  file to be varied.  The API requires the image to be a valid PNG
  file, less than 4MB, and square.

  The keyword arguments correspond to the request parameters described
  in the [image variation request][image-variation-request]
  documentation:

  - `#:n`, `#:size`, `#:format`, `#:user`

    See `openai-image`.

- (**openai-default-image-size** 512)

  Parameter for the default `#:size` value.

- (**openai-default-image-format** 'b64)

  Parameter for the default `#:format` value.

- (**image?** image)

  Type predicate for image responses.

- (**image-url** image)

  The URL of the image, if using the `url` format.

- (**image-file** image)

  The temporary filename containing the image data.

### Embedding API

- (**openai-embedding** input *[keyword arguments]*)

  Send an embedding request.  Returns an *embedding* record.

  The *input* must be a string to be embedded.

  The keyword arguments correspond to the request parameters described
  in the [embedding request][embedding-request] documentation:

  - `#:model`

    A symbol or string identifying the model to use.  Defaults to
    `text-embedding-ada-002`.

  - `#:user`

    An optional username to associate with this request.

- (**openai-default-embedding-model** 'text-embedding-ada-002)

  Parameter for the default `#:model` value.

- (**embedding?** embedding)

  Type predicate for embedding responses.

- (**embedding-vector** embedding)

  The embedding vector generated by the model.

### Text Edit API

- (**openai-text-edit** input instruction *[keyword arguments]*)

  Send an edit request.  Returns an *edit* record.

  The *input* must be a string, which is the text to be edited.  The
  *instruction* is a string describing the changes to be made.

  The keyword arguments correspond to the request parameters described
  in the [edit request][edit-request] documentation:

  - `#:model`

    A symbol or string identifying the model to use.  Defaults to
    `text-davinci-edit-001`.

  - `#:n`

    The number of responses to generate, returned as multiple values.

  - `#:temperature`

    The sampling temperature to use, a number between 0 and 2.

  - `#:top-p`

    An alternative sampling parameter, a number between 0 and 1.

- (**openai-code-edit** input instruction *[keyword arguments]*)

  Identical to `openai-text-edit`, except it defaults to the
  `code-davinci-edit-001` model.

- (**openai-default-text-edit-model** 'text-davinci-edit-001)

  Parameter for the default `#:model` value (text edit).

- (**openai-default-text-edit-temperature** *unspecified*)

  Parameter for the default `#:temperature` value (text edit).

- (**openai-default-text-edit-top-p** *unspecified*)

  Parameter for the default `#:top-p` value (text edit).

- (**openai-default-code-edit-model** 'code-davinci-edit-001)

  Parameter for the default `#:model` value (code edit).

- (**openai-default-code-edit-temperature** *unspecified*)

  Parameter for the default `#:temperature` value (code edit).

- (**openai-default-code-edit-top-p** *unspecified*)

  Parameter for the default `#:top-p` value (code edit).

- (**edit?** edit)

  Type predicate for edit responses.

- (**edit-content** edit)

  The edited content generated by the model.

### Moderation API

- (**openai-moderation** input *[keyword arguments]*)

  Send a moderation request.  Returns a *moderation* record.

  The *input* must be a string to be classified by the moderation
  model.

  The keyword arguments correspond to the request parameters described
  in the [moderation request][moderation-request] documentation:

  - `#:model`

    A symbol or string identifying the model to use.  The default is
    `*unspecified*`, which is equivalent to `text-moderation-latest`.

- (**openai-default-moderation-model** *unspecified*)

  Parameter for the default `#:model value`.

- (**moderation?** moderation)

  Type predicate for moderation responses.

- (**moderation-flagged?** moderation)

  Whether the content was in violation of OpenAI's content policy.

- (**moderation-categories** moderation)

  The list of categories which caused this content to be flagged.

- (**moderation-scores** moderation)

  An association list of the scores for this content in each category.

- (**moderation-score** moderation category)

  The moderation score for this content in the specified category.

## License

Copyright © 2023 Andrew Whatson <whatson@tailcall.au>

guile-openai is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.

guile-openai is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
Affero General Public License for more details.

You should have received a copy of the GNU Affero General Public
License along with guile-openai.  If not, see
<https://www.gnu.org/licenses/>.

See [COPYING](COPYING) for details.

[guile]: https://www.gnu.org/software/guile/
[openai-api]: https://platform.openai.com/
[geiser]: https://www.nongnu.org/geiser/
[guile-studio]: https://elephly.net/guile-studio/
[guix]: https://guix.gnu.org/
[chat-request]: https://platform.openai.com/docs/api-reference/chat/create
[image-request]: https://platform.openai.com/docs/api-reference/images/create
[image-edit-request]:
https://platform.openai.com/docs/api-reference/images/create-edit
[image-variation-request]: https://platform.openai.com/docs/api-reference/images/create-variation
[embedding-request]: https://platform.openai.com/docs/api-reference/embeddings
[moderation-request]: https://platform.openai.com/docs/api-reference/moderations
[edit-request]:
https://platform.openai.com/docs/api-reference/edits
